---
title: Typing for Python Developers

description: Get to know Python's Type System with working examples
---

import CodeSnippet from '@site/src/sandbox/CodeSnippet'

# Typing for Python Developers

*A 5‑Minute Tour with Pyrefly.*

**Goal:** In five minutes you'll know how Python's static type system _infers_, _defines_, and _composes_ types—and you'll have copy‑paste snippets to start using right away.

If you are new to Python typing, check out our [Python Typing 101 guide](../python-typing-for-beginners/).

Python's type system allows you to annotate variables so you, your teammates and your type checker can find bugs before you run your code. Think of it as documentation that's automatically validated and will help your IDE help you.

_TL;DR_
* Catch bugs **before** running the code.
* Improve editor autocomplete & refactors.
* Turn your code into living documentation.

### Types with Inference

Static analyzers can often _infer_ types from your code—no annotations required. Pyrefly takes this a step further.

<CodeSnippet
  sampleFilename="basic_inference.py"
  codeSample={`# Basic Inference
from typing import reveal_type

answer = 42
reveal_type(answer) # hover to reveal type

fruits = ["apple", "banana", "cherry"]
scores = {"math": 95, "science": 90}

def greet(name):
    return f"Hello, {name}!"

message = greet("World")
`}
/>

### Where Inference Shines ✨
- Constant assignments (`answer = 42 -> int`)
- List/tuple/dict literals with uniform elements (`names = ["A", "B"] -> list[str]`)
- Return types if parameter types are annotated:

<CodeSnippet
  sampleFilename="return_inference.py"
  codeSample={`def add(a: int, b: int):   # ✅ param annotations
    return a + b    # 🔍 return inferred -> int
`}
/>

### When to Add Hints
- Public APIs (library or service boundaries)
- Mixed collections (`list[int | str]`)
- Callable signatures (decorators, callbacks)

## Define Types Inline

### The Basics

Python's built-in types can be used to write many type hints.
<CodeSnippet
  sampleFilename="built_in_types.py"
  codeSample={`# Example: Basic Types

from typing import reveal_type

age: int = 5
reveal_type(age) # revealed type: int

age = "oops"

name: str = "John"
reveal_type(name) # revealed type: str

numbers: list[int] = [1, 2, 3]
reveal_type(numbers) # revealed type: list[int]

names: list[str] = ["John", "Jane"]
reveal_type(names) # revealed type: list[str]

person: dict[str, str] = {"name": "John", "age": "30"}
reveal_type(person) # revealed type: dict[str, str]

is_admin = True
reveal_type(is_admin) # revealed type: Literal[True]
`}
/>

### Functions
Defining the parameter and return types for a function doesn't just help prevent bugs, but it makes it easier to navigate in other files. You don't always need to define a return type - we'll do our best to infer it for you! We can't always get it right and an explicit return type will help your IDE navigate faster and more accurately.
<CodeSnippet
  sampleFilename="functions_types.py"
  codeSample={`# Example: Functions

from typing import reveal_type

def greet(name: str) -> str:
    return f"Hello, {name}!"

greet("Pyrefly")

def whatDoesThisFunctionReturnAgain(a: int, b: int):
    return a + b

reveal_type(whatDoesThisFunctionReturnAgain(2, 3)) # revealed type: int
`}
/>

## Advanced Types

### Composing Types
The real power comes from composing smaller pieces into richer shapes.

### Unions & Optional
<CodeSnippet
  sampleFilename="unions_types.py"
  codeSample={`# Union and Optional Types

from typing import Optional

def to_int(data: str | bytes | None) -> Optional[int]:
    if data is None:
        return None
    if isinstance(data, bytes):
        data = data.decode()
    return int(data)
`}
/>

### Generics

Generics allow you to define reusable functions and classes that work with multiple types. This feature enables you to write more flexible and adaptable code.

**Declaring Generic Classes:**
<CodeSnippet
  sampleFilename="generics.py"
  codeSample={`# Example: Generic Classes

from typing import reveal_type

class C[T]:
    def __init__(self, x: T):
        self.x = x
    def box(self) -> list[T]:
        return [self.x]

c = C(0)
reveal_type(c.box())  # revealed type: list[int]
`}
/>

**Declaring Type Statements:**
<CodeSnippet
  sampleFilename="type_statements.py"
  codeSample={`# Example: Type Statements
type ListOrSet[T:int] = list[T] | set[T]
`}
/>


**ParamSpec and TypeVarTuple:**
<CodeSnippet
  sampleFilename="param_spec_typevar_tuple.py"
  codeSample={`# Example: ParamSpec and TypeVarTuple
class ChildClass[T, *Ts, **P]: ...
`}
/>


### Variance Inference in Generics

When working with generics, a key question is: if one type is a subtype of another, does the subtyping relationship carry over to generic types?
For example, if `int` is a subtype of `float`, is `A[int]` also a subtype of `A[float]`?

This behavior is governed by variance:

- Covariant types preserve the direction of subtyping (`A[int]` is a subtype of `A[float]`).
- Contravariant types reverse it.
- Invariant types require an exact match.


Before [PEP 695](https://peps.python.org/pep-0695/), variance had to be declared manually and was often confusing.
Pyrefly infers the variance automatically based on how each type parameter is used - in method arguments, return values, attributes, and base classes.



**Example 1:** Covariance from Immutable Attributes (`Final`)


<CodeSnippet
  sampleFilename="variance1.py"
  codeSample={`# Example 1: Variance Inference

from typing import Final

class ShouldBeCovariant[T]:
    x: Final[T]

    def __init__(self, value: T):
        self.x = value


x1: ShouldBeCovariant[float] = ShouldBeCovariant[int](1)   # OK
x2: ShouldBeCovariant[int] = ShouldBeCovariant[float](1.0)  # ERROR!
`}
/>

**How Variance is Inferred:**

- The attribute `x` is annotated as `Final[T]`, making it immutable after initialization.
- Because `T` appears only in this read-only position, it is safe to infer `T` as covariant.
- This means:
  - You can assign `ShouldBeCovariant[int]` to a variable expecting `ShouldBeCovariant[float]` (since `int` is a subtype of `float`).
  - But the reverse is not allowed (`ShouldBeCovariant[float]` to `ShouldBeCovariant[int]`), which triggers a type error.


**Example 2:** General Variance Inference from Method and Base Class Usage

<CodeSnippet
  sampleFilename="variance2.py"
  codeSample={`# Example 2: Variance Inference

class ClassA[T1, T2, T3](list[T1]):
    def method1(self, a: T2) -> None:
        ...

    def method2(self) -> T3:
        ...

def func_a(p1: ClassA[float, int, int], p2: ClassA[int, float, float]):
    v1: ClassA[int, int, int] = p1  # ERROR!
    v2: ClassA[float, float, int] = p1  # ERROR!
    v3: ClassA[float, int, float] = p1  # OK

    v4: ClassA[int, int, int] = p2  # ERROR!
    v5: ClassA[int, int, float] = p2  # OK
`}
/>

**How Variance is Inferred:**
- `T1` appears in the base class `list[T1]`. Since list is mutable, `T1` is invariant.
- `T2` is used as the type of a method parameter (`a: T2`) so `T2` contravariant.
- `T3` is the return type of a method (`def method2() -> T3`) so `T3` is covariant.
- This means:
  - `v1` fails due to mismatched `T1` (invariant).
  - `v2` fails because `T2` expects a supertype, but gets a subtype.
  - `v4` fails because `T3` expects a subtype, but gets a supertype.

## Structural Types and Protocols
Python also employs a structural type system, often referred to as "duck typing." This concept is based on the idea that if two objects have the same shape or attributes, they can be treated as being of the same type.

### Dataclasses

Dataclasses allow you to create type-safe data structures while minimizing boilerplate.

<CodeSnippet
  sampleFilename="data_classes.py"
  codeSample={`# Example: Dataclasses

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

Point(x=0.0, y=0.0)    # OK
Point(x=0.0, y="oops") # ERROR!
`}
/>


### TypedDict

Typed dictionaries enable you to define dictionaries with specific key-value types. This feature lets you bring type safety to ad-hoc dictionary structures without major refactoring.

<CodeSnippet
  sampleFilename="typed_dict.py"
  codeSample={`# Example: TypedDict

from typing import TypedDict

class Movie(TypedDict):
    name: str
    year: int

good_movie: Movie = {"name": "Toy Story", "year": 1995} # OK
bad_movie: Movie = {"name": "The Room", "year": "2003"}  # ERROR!
`}
/>

### Overloads

Overloads allow you to define multiple function signatures for a single function. Like generics, this feature helps you write more flexible and adaptable code.

<CodeSnippet
  sampleFilename="overloads.py"
  codeSample={`# Example: Overloads

from typing import overload, reveal_type

@overload
def f(x: int) -> int: ...

@overload
def f(x: str) -> str: ...

def f(x: int | str) -> int | str:
    return x

reveal_type(f(0))  # revealed type: int
reveal_type(f("")) # revealed type: str
`}
/>

### Protocols

Protocols allows you to define interfaces without explicit inheritance. This feature helps you write more modular and composable code.

<CodeSnippet
  sampleFilename="protocols.py"
  codeSample={`# Example: Structural Typing with Protocols

from typing import Iterable, Protocol

class Writer(Protocol):
    def write(self) -> None: ...

class GoodWorld:
    def write(self) -> None:
        print("Hello world!")

class BadWorld:
    pass

def f(writer: Writer):
    pass

f(GoodWorld()) # OK
f(BadWorld())  # ERROR!
`}
/>

## Typing Features and PEPS available in each Python Version
See the full list of features available in the Python type system [here](../python-features-and-peps).

### Key Highlights Summary:
- **Inference:** Python's static analyzers can infer types from your code, reducing the need for explicit annotations. This feature enhances code readability and helps catch bugs early.
- **Defining Types:** You can define types inline using Python's built-in types, which aids in documentation and improves IDE support.
- **Advanced Types:** The guide covers advanced concepts like composing types, using unions and optionals, generics, protocols, and structural types like dataclasses and TypedDict.
- **Practical Examples:** The guide includes examples of functions, generic classes, structural typing with protocols, and more, demonstrating how to apply these concepts in real-world scenarios.
